import { exec } from 'child_process'
import { accessSync, promises, realpathSync } from 'fs'
import { resolve } from 'path'
import { promisify } from 'util'
import debug from 'debug'
import { coerce } from 'semver'
import { rm } from 'shelljs'
import type { Json, PkgJson, UserConfig } from 'ssr-types'
import type { Chunk } from '@rspack/core'
import { logWarning } from './log'

export const ssrDebug = debug('ssr')

const getCwd = () => {
	return resolve(process.cwd(), process.env.APP_ROOT ?? '')
}

const getFeDir = () => {
	return resolve(getCwd(), process.env.FE_ROOT ?? 'web')
}

const getPagesDir = () => {
	return resolve(getFeDir(), 'pages')
}

const cleanOutClientDir = () => {
	rm('-rf', resolve(getCwd(), './build/client'))
}

const getPkgJson = (): PkgJson => __non_webpack_require__(resolve(getCwd(), './package.json'))

const writeRoutes = async (routes: string, name?: string) => {
	const cwd = getCwd()
	await promises.writeFile(resolve(cwd, `./build/${name ?? 'ssr-declare-routes'}`), routes)
}

const checkContainsRev = (arr: string[], name: string) => {
	for (const val of arr) {
		if (name.includes(val)) {
			return true
		}
	}
	return false
}

const transformConfig = async () => {
	// serverless 发布无需安装 shelljs esbuild, 提前本地 build 好
	const { cp } = await import('shelljs')
	const cwd = getCwd()
	if (await accessFile(resolve(cwd, './config.js'))) {
		cp('-r', `${resolve(cwd, './config.js')}`, `${resolve(cwd, './build/config.js')}`)
	} else {
		await esbuildTransform(resolve(cwd, './config.ts'), resolve(cwd, './build/config.js'))
	}
}

export const esbuildTransform = async (from: string, to: string) => {
	if (!(await accessFile(from))) {
		await promises.writeFile(from, '')
	}
	const { build } = await import('esbuild')
	await build({
		entryPoints: [from],
		keepNames: true,
		format: 'cjs',
		bundle: !!process.env.BUNDLECONFIG,
		outfile: to,
		platform: 'node',
		mainFields: ['module', 'main'],
		treeShaking: true,
		external: process.env.BUNDLECONFIG ? ['ssr-common-utils'] : undefined,
		plugins: [
			{
				name: 'external-modules',
				setup(build) {
					build.onResolve({ filter: /^@external:/ }, (args) => ({
						path: args.path.slice(10),
						external: true
					}))
				}
			}
		]
	})
}

const transformManualRoutes = async () => {
	const cwd = getCwd()
	const declaretiveRoutes = await accessFile(resolve(getFeDir(), './route.ts')) // 是否存在自定义路由
	if (!declaretiveRoutes) {
		await promises.writeFile(resolve(cwd, './build/ssr-manual-routes.js'), '')
		return
	}
	const { transform } = await import('esbuild')
	const fileContent = (await promises.readFile(resolve(getFeDir(), './route.ts'))).toString()
	const { code } = await transform(fileContent, {
		loader: 'ts',
		format: 'esm',
		keepNames: true
	})
	const serializeCode = code.replace(/(import\([\s\S]*?\))/g, (match) => {
		return match.replace(/\s/g, '')
	})
	await promises.writeFile(resolve(cwd, './build/ssr-manual-routes.js'), serializeCode)
}

const getUserConfig = (): UserConfig => {
	const defaultConfig = resolve(getCwd(), './build/config.js')
	const userConfig = accessFileSync(defaultConfig)
		? (__non_webpack_require__(defaultConfig).userConfig ?? __non_webpack_require__(defaultConfig))
		: {} // for dynamic file
	return Object.assign(userConfig, getEnvConfig())
}

export const getEnvConfig = (): UserConfig => {
	return process.env.ssrConfig ? JSON.parse(process.env.ssrConfig) : {}
}

const cyrb53 = function (str: string, seed = 0) {
	let h1 = 0xdeadbeef ^ seed
	let h2 = 0x41c6ce57 ^ seed
	for (let i = 0, ch; i < str.length; i++) {
		ch = str.charCodeAt(i)
		h1 = Math.imul(h1 ^ ch, 2654435761)
		h2 = Math.imul(h2 ^ ch, 1597334677)
	}
	h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
	h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
	return 4294967296 * (2097151 & h2) + (h1 >>> 0)
}
export const sortByAscii = (a: string, b: string) => {
	for (let i = 0; i < Math.min(a.length, b.length); i++) {
		if (a.charCodeAt(i) !== b.charCodeAt(i)) {
			return a.charCodeAt(i) - b.charCodeAt(i)
		}
	}
	return a.length - b.length
}

export const cryptoAsyncChunkName = (chunks: Chunk[], asyncChunkMap: Record<string, string[]>) => {
	const arr = chunks.filter(Boolean)
	arr.sort((a, b) => sortByAscii(a.name ?? 'default', b.name ?? 'default')) // keep the same order of chunks
	const allChunksNames = arr.map((item) => item.name).join('~')
	const allChunksNamesArr = allChunksNames.split('~')
	const cryptoAllChunksNames = String(arr.length > 3 ? cyrb53(allChunksNames) : allChunksNames)
	if (allChunksNamesArr.length >= 2 && !asyncChunkMap?.[cryptoAllChunksNames]) {
		asyncChunkMap[cryptoAllChunksNames] = allChunksNamesArr
	}
	return cryptoAllChunksNames
}

const isFaaS = async (fun?: boolean) => {
	const result = await promises
		.access(resolve(getCwd(), fun ? 'template.yml' : 'f.yml'))
		.then(() => true)
		.catch(() => false)
	return result
}

export const checkRoute = ({
	routeItem,
	path
}: {
	routeItem?: { path: string }
	path: string
}) => {
	if (!routeItem?.path) {
		throw new Error(`
      With Path: ${path} search component failed
      If you create new folder or component file, please restart server by npm start
      `)
	}
}

const judgeFramework = () => {
	const { framework } = getUserConfig()
	if (framework) {
		return framework
	}
	const cwd = getCwd()
	const packageJSON = require(resolve(cwd, './package.json'))
	if (packageJSON.dependencies.react || packageJSON.devDependencies.react) {
		return 'ssr-plugin-react'
	} else if (packageJSON.dependencies.vue || packageJSON.devDependencies.vue) {
		const version = packageJSON.dependencies.vue || packageJSON.devDependencies.vue
		return coerce(version)!.major === 3 ? 'ssr-plugin-vue3' : 'ssr-plugin-vue'
	} else {
		throw new Error('get framework failed, please check dependencies')
	}
}

export const isReact18 = () => {
	const cwd = getCwd()
	const packageJSON = require(resolve(cwd, './package.json'))
	return packageJSON.dependencies.react && coerce(packageJSON.dependencies.react)?.major === 18
}
export const addDefaultAlias = (alias: Record<string, string>) => {
	const cwd = getCwd()
	const framework = judgeFramework()
	if (framework === 'ssr-plugin-react') {
		alias['react'] = resolve(cwd, './node_modules/react')
		alias['react-dom'] = resolve(cwd, './node_modules/react-dom')
		alias['react-router-dom'] = resolve(cwd, './node_modules/react-router-dom')
	} else {
		alias['vue$'] =
			framework === 'ssr-plugin-vue' ? 'vue/dist/vue.runtime.esm.js' : 'vue/dist/vue.runtime.esm-bundler.js'
		if (loadModuleFromFramework('pinia')) {
			alias['pinia'] = loadModuleFromFramework('pinia')
		}
	}
	if (isReact18()) {
		alias['react-dom/client'] = resolve(cwd, './node_modules/react-dom/client')
	}
	if (framework === 'ssr-plugin-vue3') {
		alias['@vue/server-renderer'] = '@vue/server-renderer/index.js'
	}
	alias['valtio'] = resolve(cwd, './node_modules/valtio')
	return alias
}
const judgeVersion = (version: string) => {
	return coerce(version)
}

const judgeServerFramework = () => {
	const cwd = getCwd()
	const packageJSON = require(resolve(cwd, './package.json'))
	if (packageJSON.dependencies['@midwayjs/decorator']) {
		return 'ssr-plugin-midway'
	} else {
		return 'ssr-plugin-nestjs'
	}
}

const checkModuleExist = (name: string) => {
	return loadModuleFromFramework(name) !== ''
}
const loadModuleFromFramework = (path: string) => {
	try {
		const framework = judgeFramework()
		const paths = resolve(getCwd(), `./node_modules/${framework}`)
		return require.resolve(path, {
			paths: [accessFileSync(paths) ? realpathSync(paths) : paths]
		})
	} catch (_) {
		return ''
	}
}

export const loadModuleFromCwd = (path: string) => {
	const cwd = getCwd()
	return resolve(cwd, `./node_modules/${path}`)
}

export const loadModuleFromWebpack = (path: string) => {
	const cwd = getCwd()
	return require.resolve(path, {
		paths: [resolve(cwd, './node_modules/ssr-webpack')]
	})
}

export const loadModuleFromRspack = (path: string) => {
	const cwd = getCwd()
	return require.resolve(path, {
		paths: [resolve(cwd, './node_modules/ssr-rspack')]
	})
}

export const loadModuleFromVite = (path: string) => {
	const cwd = getCwd()
	return require.resolve(path, {
		paths: [resolve(cwd, './node_modules/ssr-vite')]
	})
}

const processError = (err: any) => {
	if (err) {
		console.log(err)
		process.exit(1)
	}
}
const accessFile = async (file: string) => {
	const result = await promises
		.access(file)
		.then(() => true)
		.catch(() => false)
	return result
}

const accessFileSync = (file: string) => {
	let res = true
	try {
		accessSync(file)
	} catch (_error) {
		res = false
	}
	return res
}

export const getStaticConfig = () => {
	const staticConfigPath = resolve(getCwd(), './build/staticConfig.js')
	const staticConfig = accessFileSync(staticConfigPath) ? (__non_webpack_require__(staticConfigPath) as UserConfig) : {}
	return staticConfig
}

const execPromisify = promisify(exec)

const stringifyDefine = (obj: { [key: string]: Json }) => {
	for (const key in obj) {
		const val = obj[key]
		if (typeof val === 'string' && val.slice(0, 1) !== '"') {
			obj[key] = JSON.stringify(val)
		} else if (typeof val === 'object') {
			stringifyDefine(val)
		}
	}
}
export const getClientEntry = () => {
	const framework = judgeFramework()
	let defaultClientEntry = 'client-entry'
	if (framework === 'ssr-plugin-react') {
		defaultClientEntry = isReact18() ? 'react18-client-entry' : 'react17-client-entry'
	}
	return defaultClientEntry
}
export const getBuildEntry = () => {
	const framework = judgeFramework()
	const defaultClientEntry = getClientEntry()
	return {
		server: resolve(getCwd(), './node_modules', framework, './esm/entry/server-entry'),
		client: resolve(getCwd(), './node_modules', framework, `./esm/entry/${defaultClientEntry}`)
	}
}

export const getViteServerEntry = () => {
	const cwd = getCwd()
	const framework = judgeFramework()
	return resolve(cwd, `./node_modules/${framework}/esm/entry/server-entry.js`)
}

export const checkTsConfig = async () => {
	const cwd = getCwd()
	const { logWarning } = await import('./log')
	const f = judgeServerFramework()
	if (f !== 'ssr-plugin-midway') {
		return
	}
	const tsconfigExist = await accessFile(resolve(cwd, './tsconfig.json'))
	if (tsconfigExist) {
		try {
			const paths = require(resolve(cwd, './tsconfig.json')).compilerOptions.paths
			if (paths) {
				logWarning(
					'在 Midway 中不建议使用 tsconfig paths 去引用非类型文件, ref https://midwayjs.org/docs/faq/alias_path'
				)
			}
		} catch (_error) {
			// 有可能 json 文件存在注释导致 require 失败，这里 catch 一下
			console.log('检测到当前目录 tsconfig.json 文件可能存在语法错误，请检查是否存在注释或多余的符号')
		}
	}
}

export const requireWithPreserveLinks = (path: string): string | undefined => {
	const cwd = getCwd()
	const pkgPath = resolve(cwd, `./node_modules/${path}`)
	const pkgJsonPath = resolve(pkgPath, './package.json')
	if (!accessFileSync(pkgJsonPath)) {
		logWarning(`Please verify the package.json file, current program use ${path} but don't specify it in dependencies`)
		return
	}
	const pkg = require(pkgJsonPath)
	if (pkg.exports?.node) {
		const { node } = pkg.exports
		const target = typeof node === 'string' ? node : node.require || node.default
		if (target) {
			return resolve(pkgPath, target)
		}
	}
	if (pkg.main) {
		return resolve(pkgPath, pkg.main)
	}
	logWarning(`Please verify whether ${path} set correct entry file in package.json`)
}
export {
	getCwd,
	getFeDir,
	getPagesDir,
	getUserConfig,
	isFaaS,
	processError,
	accessFile,
	execPromisify,
	transformConfig,
	accessFileSync,
	judgeFramework,
	loadModuleFromFramework,
	transformManualRoutes,
	writeRoutes,
	stringifyDefine,
	judgeServerFramework,
	judgeVersion,
	cleanOutClientDir,
	checkContainsRev,
	getPkgJson,
	checkModuleExist
}
